# Building a Custom Retry Plugin

This guide shows how to create a custom retry plugin with advanced features like circuit breaker patterns, fallback components, and intelligent error handling for Module Federation applications.

## Overview

When building distributed applications with Module Federation, handling offline remotes gracefully is crucial for maintaining application stability. A custom retry plugin can provide sophisticated error handling beyond basic retry mechanisms.

## Core Features

A robust retry plugin should include:

- **Retry Logic**: Configurable retry attempts with exponential backoff
- **Circuit Breaker Pattern**: Prevent cascading failures by temporarily disabling failed remotes
- **Fallback Components**: Graceful UI degradation when remotes are unavailable
- **Timeout Handling**: Prevent hanging requests
- **Intelligent State Management**: Track remote health and failure patterns

## Implementation

### Basic Plugin Structure

```ts
import type { ModuleFederationRuntimePlugin } from "@module-federation/enhanced/runtime";

interface RetryConfig {
  enableLogging?: boolean;
  fallbackTimeout?: number;
  retryAttempts?: number;
  retryDelay?: number;
  enableCircuitBreaker?: boolean;
  circuitBreakerThreshold?: number;
  circuitBreakerResetTimeout?: number;
}

const customRetryPlugin = (config: RetryConfig = {}): ModuleFederationRuntimePlugin => {
  const {
    enableLogging = true,
    fallbackTimeout = 5000,
    retryAttempts = 2,
    retryDelay = 1000,
    enableCircuitBreaker = true,
    circuitBreakerThreshold = 3,
    circuitBreakerResetTimeout = 60000,
  } = config;

  return {
    name: "custom-retry-plugin",
    // Implementation details below...
  };
};
```

### State Management

Track remote health using a state management system:

```ts
interface RemoteState {
  failureCount: number;
  lastFailureTime: number;
  isCircuitOpen: boolean;
}

// Track remote states for circuit breaker pattern
const remoteStates = new Map<string, RemoteState>();

const getRemoteState = (remoteId: string): RemoteState => {
  if (!remoteStates.has(remoteId)) {
    remoteStates.set(remoteId, {
      failureCount: 0,
      lastFailureTime: 0,
      isCircuitOpen: false,
    });
  }
  return remoteStates.get(remoteId)!;
};

const updateRemoteState = (remoteId: string, isSuccess: boolean) => {
  const state = getRemoteState(remoteId);
  
  if (isSuccess) {
    // Reset on success
    state.failureCount = 0;
    state.isCircuitOpen = false;
  } else {
    // Increment failure count
    state.failureCount++;
    state.lastFailureTime = Date.now();
    
    // Open circuit if threshold reached
    if (enableCircuitBreaker && state.failureCount >= circuitBreakerThreshold) {
      state.isCircuitOpen = true;
      
      // Auto-reset circuit after timeout
      setTimeout(() => {
        state.isCircuitOpen = false;
        state.failureCount = 0;
      }, circuitBreakerResetTimeout);
    }
  }
};
```

### Retry Logic with Timeout

Implement exponential backoff with timeout protection:

```ts
const withRetry = async <T>(
  operation: () => Promise<T>,
  remoteId: string,
  attempts = retryAttempts
): Promise<T> => {
  let lastError: Error;
  
  for (let i = 0; i < attempts; i++) {
    try {
      const result = await Promise.race([
        operation(),
        new Promise<never>((_, reject) => 
          setTimeout(() => reject(new Error('Request timeout')), fallbackTimeout)
        )
      ]);
      
      // Success - update state
      updateRemoteState(remoteId, true);
      return result;
    } catch (error) {
      lastError = error as Error;
      
      // Wait before retry (exponential backoff)
      if (i < attempts - 1) {
        await new Promise(resolve => setTimeout(resolve, retryDelay * (i + 1)));
      }
    }
  }
  
  // All attempts failed
  updateRemoteState(remoteId, false);
  throw lastError!;
};
```

### Error Handling by Lifecycle

Handle different error types based on the failure stage:

```ts
async errorLoadRemote(args) {
  const { id, error, from, lifecycle } = args;
  const remoteId = id || from || 'unknown';

  // Different handling based on lifecycle
  switch (lifecycle) {
    case 'beforeRequest':
    case 'afterResolve':
      // Manifest loading failed
      const state = getRemoteState(remoteId);
      if (state.isCircuitOpen) {
        return createFallbackModule(remoteId, error);
      }
      
      try {
        // Retry the original request
        return await withRetry(() => args.origin(), remoteId);
      } catch (retryError) {
        return createFallbackModule(remoteId, error);
      }

    case 'onLoad':
      // Module loading failed after manifest was loaded
      return () => createFallbackModule(remoteId, error);

    case 'beforeLoadShare':
      // Shared dependency loading failed
      return createFallbackModule(remoteId, error);

    default:
      return createFallbackModule(remoteId, error);
  }
}
```

### Fallback Components

Create graceful fallback UI components:

```ts
const createFallbackComponent = (remoteId: string, error?: Error) => {
  const FallbackComponent = async () => {
    // Dynamically import React to avoid eager loading issues
    const React = await import('react');
    
    // Build error details safely
    const errorDetails = error ? [
      React.createElement("details", { key: "error" }, [
        React.createElement("summary", { key: "summary" }, "Error Details"),
        React.createElement("pre", { 
          key: "error-details",
          style: { 
            overflow: "auto",
            fontSize: "12px",
            textAlign: "left" as const
          }
        }, error?.message || String(error))
      ])
    ] : null;
    
    return React.createElement("div", {
      style: {
        padding: "16px",
        margin: "8px",
        border: "2px dashed #ffa39e",
        borderRadius: "8px",
        backgroundColor: "#fff2f0",
        color: "#cf1322",
        textAlign: "center" as const,
      },
    }, [
      React.createElement("h3", { key: "title" }, "Remote Module Unavailable"),
      React.createElement("p", { key: "description" }, 
        `The remote module "${remoteId}" is currently offline.`),
      errorDetails
    ].filter(Boolean));
  };

  FallbackComponent.displayName = `FallbackComponent_${remoteId}`;
  return FallbackComponent;
};

const createFallbackModule = (remoteId: string, error?: Error) => {
  try {
    const FallbackComponent = createFallbackComponent(remoteId, error);
    
    return {
      __esModule: true,
      default: FallbackComponent,
      [remoteId]: FallbackComponent,
    };
  } catch (createError) {
    // If fallback creation fails, return minimal module
    console.error('Failed to create fallback module:', createError);
    return {
      __esModule: true,
      default: () => null,
    };
  }
};
```

## Usage

### Configuration

```ts
import { init } from '@module-federation/enhanced/runtime';
import customRetryPlugin from './custom-retry-plugin';

init({
  name: 'host',
  remotes: [
    {
      name: 'remote1',
      entry: 'http://localhost:3001/remoteEntry.js'
    }
  ],
  plugins: [
    customRetryPlugin({
      enableLogging: true,
      retryAttempts: 3,
      retryDelay: 1000,
      fallbackTimeout: 5000,
      enableCircuitBreaker: true,
      circuitBreakerThreshold: 5,
      circuitBreakerResetTimeout: 30000,
    })
  ]
});
```

### Integration with Share Strategy

Be aware of how `shareStrategy` affects remote loading:

```ts
// Version-first strategy eagerly loads remotes
// This can trigger failures during app startup
shareStrategy: 'version-first'

// Loaded-first strategy loads remotes on-demand
// Failures occur only when modules are actually requested
shareStrategy: 'loaded-first' // Recommended for offline scenarios
```

For applications that need to handle offline scenarios gracefully, consider using `loaded-first` strategy combined with your retry plugin.

## Best Practices

1. **Graceful Degradation**: Always provide meaningful fallback UI
2. **Circuit Breaker**: Prevent cascading failures with circuit breaker pattern
3. **Logging**: Include comprehensive logging for debugging
4. **Performance**: Cache fallback modules to avoid recreation
5. **Recovery**: Allow automatic recovery when remotes come back online
6. **Configuration**: Make retry behavior configurable per environment

## Complete Example

Here's a simplified version of a production-ready retry plugin:

```ts
import type { ModuleFederationRuntimePlugin } from "@module-federation/enhanced/runtime";
import type { ComponentType } from "react";

interface OfflineFallbackConfig {
  enableLogging?: boolean;
  fallbackTimeout?: number;
  retryAttempts?: number;
  retryDelay?: number;
  enableCircuitBreaker?: boolean;
  circuitBreakerThreshold?: number;
  circuitBreakerResetTimeout?: number;
  fallbackComponents?: Record<string, ComponentType>;
}

const enhancedOfflineFallbackPlugin = (
  config: OfflineFallbackConfig = {}
): ModuleFederationRuntimePlugin => {
  const {
    enableLogging = true,
    fallbackTimeout = 5000,
    retryAttempts = 2,
    retryDelay = 1000,
    enableCircuitBreaker = true,
    circuitBreakerThreshold = 3,
    circuitBreakerResetTimeout = 60000,
    fallbackComponents = {},
  } = config;

  const remoteStates = new Map<string, any>();
  const fallbackCache = new Map<string, ComponentType>();

  const log = (message: string, ...args: any[]) => {
    if (enableLogging) {
      console.warn(`[OfflineFallbackPlugin] ${message}`, ...args);
    }
  };

  const createFallbackComponent = (remoteId: string, error?: Error) => {
    if (fallbackComponents[remoteId]) {
      return fallbackComponents[remoteId];
    }

    const FallbackComponent = async () => {
      // Dynamically import React to avoid eager loading issues
      const React = await import('react');
      
      return React.createElement("div", {
        style: {
          padding: "16px",
          border: "2px dashed #ffa39e",
          borderRadius: "8px",
          backgroundColor: "#fff2f0",
          color: "#cf1322",
          textAlign: "center",
        },
      }, [
        React.createElement("h3", { key: "title" }, "Remote Module Unavailable"),
        React.createElement("p", { key: "description" }, 
          `The remote module "${remoteId}" is currently offline.`),
      ]);
    };

    return FallbackComponent;
  };

  return {
    name: "enhanced-offline-fallback-plugin",
    
    async errorLoadRemote(args) {
      const { id, error, lifecycle } = args;
      log(`Remote loading failed: ${id}`, { lifecycle, error: error?.message });

      switch (lifecycle) {
        case 'afterResolve':
          // Manifest loading failed
          return {
            id: 'fallback',
            name: 'fallback',
            metaData: {},
            shared: [],
            remotes: [],
            exposes: []
          };

        case 'onLoad':
          // Module loading failed
          return () => ({
            __esModule: true,
            default: createFallbackComponent(id, error),
          });

        default:
          return createFallbackComponent(id, error);
      }
    },

    onLoad(args) {
      log(`Successfully loaded remote: ${args.id}`);
      return args;
    },
  };
};

export default enhancedOfflineFallbackPlugin;
```

### Usage in Rspack Configuration

```ts
import { ModuleFederationPlugin } from "@module-federation/enhanced/rspack";
import enhancedOfflineFallbackPlugin from "./enhanced-offline-fallback-plugin";

export default defineConfig({
  plugins: [
    new ModuleFederationPlugin({
      name: "hostApp",
      shareStrategy: "loaded-first", // Recommended for offline handling
      remotes: {
        "remote-app": "remoteApp@http://localhost:8081/remote-mf-manifest.json",
      },
      runtimePlugins: [
        "./enhanced-offline-fallback-plugin.ts",
      ],
    }),
  ],
});
```

## Advanced Features

Consider adding these advanced features:

- **Health Checks**: Periodic health checks for circuit breaker recovery
- **Alternative Sources**: Try loading from multiple manifest URLs
- **Metrics**: Collect metrics on remote reliability
- **User Notifications**: Notify users about offline remotes
- **Progressive Enhancement**: Gracefully handle partial functionality

This approach provides robust error handling while maintaining a good user experience even when remote modules are unavailable.